package run.yiqi.blog.controller;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaBuilder.In;
import javax.servlet.http.HttpServletResponse;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import run.yiqi.blog.dao.EndUserDao;
import run.yiqi.blog.dao.IssueCommentDao;
import run.yiqi.blog.dao.IssueDao;
import run.yiqi.blog.dao.TagOptionDao;
import run.yiqi.blog.dao.TagTypeDao;
import run.yiqi.blog.model.Issue;
import run.yiqi.blog.model.IssueComment;
import run.yiqi.blog.model.TagOption;
import run.yiqi.blog.service.EmailService;
import run.yiqi.blog.service.WorkFlowService;
import run.yiqi.blog.vo.EmailVo;
import run.yiqi.blog.vo.IssueCommentVo;
import run.yiqi.blog.vo.IssueVo;
import run.yiqi.blog.vo.TagOptionVo;
import run.yiqi.util.DateTimeUtil;
import run.yiqi.util.QueryCondition;
import run.yiqi.util.StringUtil;
import run.yiqi.util.Util;

@Slf4j
@Controller
public class IssueAdminController {
    @Lazy @Autowired private IssueDao issueDao;
    @Lazy @Autowired private IssueCommentDao issueCommentDao;
    @Lazy @Autowired private TagTypeDao tagTypeDao;
    @Lazy @Autowired private TagOptionDao tagOptionDao;
    @Lazy @Autowired private EndUserDao endUserDao;
    @Lazy @Autowired private EmailService emailService;
    @Lazy @Autowired private WorkFlowService workFlowService;
    @Lazy @Autowired private HttpServletResponse response;

    private static final String PROCESS_DEFINITION_KEY = "issueTrackingProcess";

    
    
    
    // 列举Issues
    @ResponseBody
    @RequestMapping(value = { "/api/admin/issueadmin/query/listIssues" }, method = RequestMethod.POST)
    public QueryCondition<List<IssueVo>> listIssues(@RequestBody QueryCondition<List<IssueVo>> queryCondition) {
        log.info("listIssues ==> " + queryCondition.toString());

        PageRequest pageRequest = null;
        if (!StringUtil.isEmpty(queryCondition.getSort().getField())) {
            pageRequest = PageRequest.of(queryCondition.getPage().getCurrentPage() - 1,
                    queryCondition.getPage().getPageSize(),
                    Sort.by(queryCondition.getSort().getDirection(), queryCondition.getSort().getField()));
        } else {
            pageRequest = PageRequest.of(queryCondition.getPage().getCurrentPage() - 1,
                    queryCondition.getPage().getPageSize(), Sort.by(Sort.Direction.DESC, "createDate"));
        }

        Page<Issue> issues = this.issueDao.findAll(new Specification<Issue>() {
            private static final long serialVersionUID = 1L;

            @Override
            public Predicate toPredicate(Root<Issue> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicates = new ArrayList<Predicate>();

                queryCondition.getFilters().stream().forEach(filter -> {
                    switch (filter.getField()) {
                    case "id":
                        In<String> idin = criteriaBuilder.in(root.get("id"));
                        Arrays.asList(filter.getValues()).forEach(v -> {
                            idin.value(v.toString());
                        });
                        predicates.add(idin);
                        break;

                    case "keyword":
                        // title/comment/content
                        if (filter.getValues()[0].toString().trim().length() > 0) {
                            Predicate keyword = criteriaBuilder.or(
                                    criteriaBuilder.like(root.get("title").as(String.class),
                                            "%" + filter.getValues()[0].toString().trim() + "%"),
                                    criteriaBuilder.like(root.get("content").as(String.class),
                                            "%" + filter.getValues()[0].toString().trim() + "%"),
                                    criteriaBuilder.like(
                                            root.join("issueComments", JoinType.LEFT).get("content").as(String.class),
                                            "%" + filter.getValues()[0].toString().trim() + "%"));
                            predicates.add(keyword);
                        }
                        break;

                    case "status":
                        if (filter.getValues().length > 0) {
                            In<Integer> statusin = criteriaBuilder.in(root.get("status").as(Integer.class));
                            Arrays.asList(filter.getValues()).forEach(v -> {
                                statusin.value(Integer.parseInt(v.toString()));
                            });
                            predicates.add(statusin);
                        }
                        break;

                    case "issueBy":
                        if (filter.getValues().length > 0) {
                            In<String> issueBy = criteriaBuilder.in(root.get("issueBy").as(String.class));
                            Arrays.asList(filter.getValues()).forEach(v -> {
                                issueBy.value(v.toString());
                            });
                            predicates.add(issueBy);
                        }
                        break;

                    case "issueDate":
                        if (filter.getValues().length == 2) {
                            predicates.add(criteriaBuilder.between(root.get("issueDate").as(LocalDateTime.class),
                                    LocalDateTime.parse(filter.getValues()[0] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002)),
                                    LocalDateTime.parse(filter.getValues()[1] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002))));
                        }
                        break;

                    case "handleBy":
                        if (filter.getValues().length > 0) {
                            In<String> handleBy = criteriaBuilder.in(root.get("handleBy").as(String.class));
                            Arrays.asList(filter.getValues()).forEach(v -> {
                                handleBy.value(v.toString());
                            });
                            predicates.add(handleBy);
                        }
                        break;

                    case "handleDate":
                        if (filter.getValues().length == 2) {
                            predicates.add(criteriaBuilder.between(root.get("handleDate").as(LocalDateTime.class),
                                    LocalDateTime.parse(filter.getValues()[0] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002)),
                                    LocalDateTime.parse(filter.getValues()[1] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002))));
                        }
                        break;

                    case "importBy":
                        if (filter.getValues().length > 0) {
                            In<String> importBy = criteriaBuilder.in(root.get("importBy").as(String.class));
                            Arrays.asList(filter.getValues()).forEach(v -> {
                                importBy.value(v.toString());
                            });
                            predicates.add(importBy);
                        }
                        break;

                    case "createBy":
                        if (filter.getValues().length > 0) {
                            In<String> createBy = criteriaBuilder.in(root.get("createBy").as(String.class));
                            Arrays.asList(filter.getValues()).forEach(v -> {
                                createBy.value(v.toString());
                            });
                            predicates.add(createBy);
                        }
                        break;

                    case "createDate":
                        if (filter.getValues().length == 2) {
                            predicates.add(criteriaBuilder.between(root.get("createDate").as(LocalDateTime.class),
                                    LocalDateTime.parse(filter.getValues()[0] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002)),
                                    LocalDateTime.parse(filter.getValues()[1] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002))));
                        }
                        break;

                    case "updateBy":
                        if (filter.getValues().length > 0) {
                            In<String> modifyBy = criteriaBuilder.in(root.get("updateBy").as(String.class));
                            Arrays.asList(filter.getValues()).forEach(v -> {
                                modifyBy.value(v.toString());
                            });
                            predicates.add(modifyBy);
                        }
                        break;

                    case "updateDate":
                        if (filter.getValues().length == 2) {
                            predicates.add(criteriaBuilder.between(root.get("updateDate").as(LocalDateTime.class),
                                    LocalDateTime.parse(filter.getValues()[0] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002)),
                                    LocalDateTime.parse(filter.getValues()[1] + " 00:00:00",
                                            DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002))));
                        }
                        break;

                    case "tags":
                        if (filter.getValues().length > 0) {
                            In<String> tags = criteriaBuilder
                                    .in(root.join("tags", JoinType.LEFT).get("optionid").as(String.class));
                            Arrays.asList(filter.getValues()).forEach(v -> {
                                tags.value(v.toString());
                            });
                            predicates.add(tags);
                        }
                        break;

                    default:
                        break;

                    }
                });

                if (root.getJoins().stream().map(j -> j.getAttribute().getName()).filter(n -> "tags".equals(n))
                        .count() == 0) {
                    root.join("tags", JoinType.LEFT);
                }
                if (root.getJoins().stream().map(j -> j.getAttribute().getName()).filter(n -> "issueComments".equals(n))
                        .count() == 0) {
                    root.join("issueComments", JoinType.LEFT);
                }

                query.distinct(true);

                return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
            }

        }, pageRequest);

        queryCondition.getPage().setTotal(issues.getTotalElements());
        queryCondition.getPage().setPageSize(queryCondition.getPage().getPageSize());
        queryCondition.getPage().setCurrentPage(queryCondition.getPage().getCurrentPage());
        queryCondition.setData(new ArrayList<IssueVo>());

        issues.stream().distinct().forEach(issue -> {
            IssueVo issueVo = this.convertIssue2IssueVo(issue);
            queryCondition.getData().add(issueVo);
        });

        if (queryCondition.getData().size() == 0 && queryCondition.getPage().getCurrentPage() > 1) {
            queryCondition.getPage().setCurrentPage(queryCondition.getPage().getCurrentPage() - 1);
            queryCondition.getData().clear();
            queryCondition.getData().addAll(this.listIssues(queryCondition).getData());
        }

        return queryCondition;
    }
    
    //根据IssueID获取Issue详细信息
    @ResponseBody
    @RequestMapping(value = { "/api/admin/issueadmin/query/getIssue/{id}" }, method = RequestMethod.GET)
    public IssueVo getIssue(@PathVariable Integer id) {
        log.info("getIssue");
        Issue issue = this.issueDao.findById(id).get();
        IssueVo issueVo = this.convertIssue2IssueVo(issue);
        // issueVo.setContent(issue.getContent());
        return issueVo;
    }
    
    
    
    
    
    //新增问题
    @Transactional
    @SneakyThrows
    @ResponseBody
    @RequestMapping(value = "/api/admin/issueadmin/submitnewdraftissue", method = RequestMethod.POST)
    public void submitNewDraftIssue(@RequestBody IssueVo issueVo) {
        this.addDraftIssue(issueVo);

        Issue issue = this.issueDao.findById(issueVo.getId()).get();
        //1: Open, 2: Ready to test, 3: Close 4: Discard 5: Draft
        issue.setStatus(1);
        this.issueDao.saveAndFlush(issue);
        
        this.flowOper4SubmitDraftIssue(issue);
        
        this.sendEmailToUser(issueVo, issue);
    }
    //新增问题
    @Transactional
    @SneakyThrows
    @ResponseBody
    @RequestMapping(value = "/api/admin/issueadmin/submitolddraftissue", method = RequestMethod.POST)
    public void submitOldDraftIssue(@RequestBody IssueVo issueVo) {
        Issue issue = this.issueDao.findById(issueVo.getId()).get();
        //1: Open, 2: Ready to test, 3: Close 4: Discard 5: Draft
        issue.setStatus(1);
        this.issueDao.saveAndFlush(issue);
        
        this.flowOper4SubmitDraftIssue(issue);
        
        this.sendEmailToUser(issueVo, issue);
    }    
    //新增问题
    @Transactional
    @SneakyThrows
    @ResponseBody
    @RequestMapping(value = "/api/admin/issueadmin/adddraftissue", method = RequestMethod.POST)
    public void addDraftIssue(@RequestBody IssueVo issueVo) {
        // 新建Issue
        Issue issue = new Issue();
        this.convertIssueVo2Issue(issueVo, issue);
        //1: Open, 2: Solved, 3: Close 4: Discard 5: Draft
        issue.setStatus(5);
        this.issueDao.saveAndFlush(issue);

        // 处理comments
        IssueComment issueComment = new IssueComment();
        issueComment.setIssue(issue);
        issueComment.setContent(Util.getCurrentUsername() + "create draft issue.");
        issue.getIssueComments().add(issueComment);
        this.issueCommentDao.saveAndFlush(issueComment);

        // 工作流操作
        this.flowOper4AddNewDraftIssue(issue);
        
        issueVo.setId(issue.getId());
    }
    //修改问题
    @Transactional
    @SneakyThrows
    @ResponseBody
    @RequestMapping(value = "/api/admin/issueadmin/editolddraftissue", method = RequestMethod.POST)
    public IssueVo editOldDraftIssue(@RequestBody IssueVo issueVo) {

        // 修改Issue
        Issue issue = issueDao.findById(issueVo.getId()).get();
        this.convertIssueVo2Issue(issueVo, issue);
        this.issueDao.saveAndFlush(issue);

        // 修改Issue状态到Close或者Discard
        if (issue.getStatus() == 3 || issue.getStatus() == 4) {
            // 当状态为Close或Discard的时候,
            this.sendEmailToUser(issueVo, issue);
        }
        return issueVo;
    }
    
    
    
    
    //处理问题
    @Transactional
    @SneakyThrows
    @ResponseBody
    @RequestMapping(value = "/api/admin/issueadmin/handleissue", method = RequestMethod.POST)
    public void handleIssue(@RequestBody IssueCommentVo issueCommentVo) {
        //修改Issue
        Issue issue = issueDao.findById(issueCommentVo.getIssueid()).get();
        
        // 处理comments
        IssueComment issueComment = new IssueComment();
        issueComment.setIssue(issue);
        issueComment.setContent(issueCommentVo.getContent());

        issue.getIssueComments().add(issueComment);
        this.issueCommentDao.saveAndFlush(issueComment);
        
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("issueId", issueCommentVo.getIssueid());
        List<Task> tasks = this.workFlowService.listActiveTasks(PROCESS_DEFINITION_KEY, Util.getCurrentUsername() ,variables);
        
        switch(issueCommentVo.getHandleResult()) {
        case "comment": 
            //do nothing
            break;
        
        //1: Open, 2: Solved, 3: Close 4: Discard 5: Draft
        case "reject":
            issue.setStatus(5);
            variables.put("asignee", issue.getCreateBy());
            variables.put("handleResult", issueCommentVo.getHandleResult());
            tasks.forEach(task->this.workFlowService.complete(task.getId(), variables));
            break;
            
        case "solved":
            issue.setStatus(2);
            variables.put("asignee", issue.getCreateBy());
            variables.put("handleResult", issueCommentVo.getHandleResult());
            tasks.forEach(task->this.workFlowService.complete(task.getId(), variables));
            break;
            
        case "pass":
            issue.setStatus(3);
            variables.put("asignee", issue.getHandleBy());
            variables.put("testResult", "pass");
            tasks.forEach(task->this.workFlowService.complete(task.getId(), variables));
            break;

        case "fail":
            issue.setStatus(1);
            variables.put("asignee", issue.getHandleBy());
            variables.put("testResult", "fail");
            tasks.forEach(task->this.workFlowService.complete(task.getId(), variables));
            break;

        case "cancel":
            issue.setStatus(4);
            variables.put("asignee", issue.getHandleBy());
            variables.put("testResult", "cancel");
            tasks.forEach(task->this.workFlowService.complete(task.getId(), variables));
            break;
        }
        this.issueDao.saveAndFlush(issue);
    }

    // 删除Issue
    @SneakyThrows
    @ResponseBody
    @RequestMapping(value = "/api/admin/issueadmin/deleteIssue/{id}", method = RequestMethod.GET)
    public void deleteIssue(@PathVariable Integer id) {
        log.debug("deleteIssue");
        //根据issue获取对应的流程实例id
        Map<String, Object> variables = new HashMap<>();
        variables.put("issueId", id);
        List<ProcessInstance> processInstances = this.workFlowService.listProcessInstances(variables);
        processInstances.forEach(p->this.workFlowService.deleteProcessInstance(p.getProcessInstanceId()));
        this.issueDao.deleteById(id);
        return;
    }
    
    @SneakyThrows
    @ResponseBody
    @RequestMapping(value = "/api/admin/issueadmin/showFlowImg/{issueId}", method = RequestMethod.GET)    
    public void showFlowImg(@PathVariable Integer issueId) {
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("issueId", issueId);
        List<ProcessInstance> processInstances = this.workFlowService.listProcessInstances(PROCESS_DEFINITION_KEY, variables);
        
        if(processInstances.size()==1) {
            this.workFlowService.showImg(processInstances.get(0).getProcessInstanceId(), response);
        }
    }
    
    
    
    
    // 将Issue转换为IssueVo对象
    private IssueVo convertIssue2IssueVo(Issue issue) {
        IssueVo issueVo = new IssueVo();
        issueVo.setId(issue.getId());
        issueVo.setTitle(issue.getTitle());
        issueVo.setContent(issue.getContent());
        issueVo.setStatus(issue.getStatus());

        issueVo.setCreateDate(
                issue.getCreateDate().format(DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT003)));
        issueVo.setUpdateDate(
                issue.getUpdateDate().format(DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT003)));
        issueVo.setHandleDate(
                issue.getHandleDate().format(DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT003)));
        issueVo.setIssueDate(issue.getIssueDate().format(DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT003)));

        issueVo.setHandleBy(issue.getHandleBy());
        //issueVo.setImportBy(issue.getImportBy());
        issueVo.setCreateBy(issue.getCreateBy());
        issueVo.setUpdateBy(issue.getUpdateBy());
        issueVo.setIssueBy(issue.getIssueBy());

        issueVo.setTags(issue.getTags().stream().map(t -> convertTag2TagVo(t)).collect(Collectors.toList()));

        issueVo.setIssueComments(issue.getIssueComments().stream().map(c -> this.convertIssueComment2IssueCommentVo(c))
                .sorted((x, y) -> x.getCreateDate().compareTo(y.getCreateDate())).collect(Collectors.toList()));

        return issueVo;
    }

    // 将IssueVo转换为Issue对象
    private void convertIssueVo2Issue(IssueVo issueVo, Issue issue) {
        if (!StringUtil.isEmpty(issueVo.getContent())) {
            issue.setContent(issueVo.getContent());
        }
        issue.setTitle(issueVo.getTitle());

        issue.setHandleBy(issueVo.getHandleBy());
        //issue.setImportBy(issueVo.getImportBy());
        issue.setIssueBy(issueVo.getIssueBy());
        
        issue.setHandleDate(LocalDateTime.parse(issueVo.getHandleDate() + " 00:00:00",
                DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002)));
        issue.setIssueDate(LocalDateTime.parse(issueVo.getIssueDate() + " 00:00:00",
                DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT002)));

        
        issue.setTags(issueVo.getTags().stream().map(tv -> this.tagOptionDao.findById(tv.getOptionid()).get()).collect(Collectors.toSet()));
        issue.getTags().forEach(t -> t.getIssues().add(issue));
        
    }
    
    // 将Tag对象转换为TagVo对象
    private TagOptionVo convertTag2TagVo(TagOption tagOption) {
        TagOptionVo tagOptionVo = new TagOptionVo();
        tagOptionVo.setOptionid(tagOption.getOptionid());
        tagOptionVo.setOptionname(tagOption.getOptionname());
        tagOptionVo.setTypeid(tagOption.getTagtype().getTypeid());
        return tagOptionVo;
    }

    // 将issueComment对象转换为issuecommentVo对象
    private IssueCommentVo convertIssueComment2IssueCommentVo(IssueComment issueComment) {
        IssueCommentVo issueCommentVo = new IssueCommentVo();
        issueCommentVo.setId(issueComment.getId());
        issueCommentVo.setContent(issueComment.getContent());
        issueCommentVo.setCreateBy(issueComment.getCreateBy());
        issueCommentVo.setCreateDate(
                issueComment.getCreateDate().format(DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT003)));
        issueCommentVo.setIssueid(issueComment.getIssue().getId());
        issueCommentVo.setUpdateBy(issueComment.getUpdateBy());
        issueCommentVo.setUpdateDate(
                issueComment.getUpdateDate().format(DateTimeFormatter.ofPattern(DateTimeUtil.DATETIMEFORMAT003)));
        return issueCommentVo;
    }

    //创建Issue时,同步更新工作流表
    private void flowOper4AddNewDraftIssue(Issue issue) {
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("issueId", issue.getId());
        variables.put("asignee", issue.getCreateBy());
        this.workFlowService.startProcessInstance(PROCESS_DEFINITION_KEY, variables);
    }
    private void flowOper4SubmitDraftIssue(Issue issue) {
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("issueId", issue.getId());
        variables.put("asignee", issue.getCreateBy());
        List<ProcessInstance> processInstances = this.workFlowService.listProcessInstances(PROCESS_DEFINITION_KEY, variables);

        if(processInstances.size()==1) {
            List<Task> tasks = this.workFlowService.listActiveTasks(processInstances.get(0).getProcessInstanceId(), null);
            tasks.forEach(task->{
                variables.put("asignee", issue.getHandleBy());
                this.workFlowService.complete(task.getId(), variables);
            });
        }
    }
    
    
    
    // 发邮件给相关用户
    @SneakyThrows
    private void sendEmailToUser(IssueVo issueVo, Issue issue) {
        EmailVo emailVo = new EmailVo();
        emailVo.getTos().add(this.endUserDao.findByUsername(issue.getHandleBy()).getEmail());
        emailVo.getCcs().add(this.endUserDao.findByUsername(issue.getUpdateBy()).getEmail());
        emailVo.setSubject("[Issue Tracking] Issue #" + issue.getId() + " is " + issueVo.getStatusLabel() + " by  " + issue.getCreateBy());

        StringBuffer msg = new StringBuffer("Dear ").append(issue.getHandleBy()).append(": ");
        msg.append("<br/>").append(issue.getIssueComments().get(issue.getIssueComments().size() - 1).getContent());
        msg.append("<br/>Please click <a href='").append(this.emailService.getDeployUrl()).append("#/ui/admin/issue/").append(issue.getId()).append("'>here</a> to check.");
        emailVo.setContent(msg.toString());

        emailService.sendEmail(emailVo);
    }
}